Skip to content

feat(overlay): show popup card when caret is mid-line#624

Open
FuJacob wants to merge 1 commit into
mainfrom
feat-midline-caret-popup
Open

feat(overlay): show popup card when caret is mid-line#624
FuJacob wants to merge 1 commit into
mainfrom
feat-midline-caret-popup

Conversation

@FuJacob
Copy link
Copy Markdown
Owner

@FuJacob FuJacob commented Jun 6, 2026

Summary

When the caret sits mid-line (real characters follow it before the next line break), inline ghost text has no home: it would render on top of the text after the caret. This promotes any such presentation to the popup (mirror) card instead, anchored to the caret line. It is the first step toward fill-in-middle (FIM) completions, which structurally need the card because a mid-line completion cannot be drawn inline.

The signal is the existing CaretLinePosition.isAtEndOfLine, so an end-of-line caret that still has later paragraphs below it stays inline (only same-line trailing characters trigger the card).

Validation

swiftlint lint --quiet <6 changed source files>
# exit 0

xcodebuild -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' \
  build-for-testing -derivedDataPath build/DerivedData CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO
# ** TEST BUILD SUCCEEDED **

xcodebuild test-without-building ... \
  -only-testing:CotabbyTests/CompletionRenderModePolicyTests \
  -only-testing:CotabbyTests/CaretLinePositionTests \
  -only-testing:CotabbyTests/MirrorOverlayLayoutTests
# Executed 39 tests, with 0 failures

App-hosted tests were run locally with signing disabled. The 7 new policy tests cover the auto / always-inline / always-mirror / per-app-override matrix at both end-of-line and mid-line carets; existing policy, caret-position, and mirror-layout suites still pass.

Linked issues

No issue number provided. Groundwork for FIM completions.

Risk / rollout notes

  • Behavior change to an existing flow: a suggestion generated with the caret mid-line (e.g. the mid-word continuations that already fire via MidWordContinuationPolicy) previously rendered as inline ghost text overlapping the trailing characters. It now renders in the popup card. This is a net improvement for that case, but it is a visible change.
  • The mid-line rule deliberately overrides an explicit "Inline" appearance preference, because inline cannot render mid-line without overlap. If we would rather honor the pin and leave those users on inline mid-line, the override is a one-line change in CompletionRenderModePolicy.mode (gate the promotion on .auto only). Flagging for a product call.
  • No schema, settings, or project.yml / pbxproj migrations. No new files (test changes live in existing files, so XcodeGen does not drift).
  • Mirror-card anchoring for the new .caretMidLine reason reuses the trustworthy-geometry path (anchors to the caret line), so there is no new positioning math.

Greptile Summary

When the caret sits mid-line (real characters follow it on the same line), inline ghost text can't render without painting over those trailing characters. This PR detects that condition via the existing CaretLinePosition.isAtEndOfLine signal, promotes any inline result to the popup card with a new caretMidLine mirror reason, and anchors the card to the caret rect (geometry is trustworthy here) rather than the input-field rect.

  • CompletionRenderModePolicy is split into a public mode(for:bundleIdentifier:) wrapper and a private preferenceMode helper; the wrapper applies the mid-line promotion as a single, self-contained rule that upgrades .inline only, leaving existing mirror reasons (e.g. .caretGeometryEstimated) unchanged.
  • SuggestionOverlayGeometry gains isCaretAtEndOfLine: Bool (defaults to true for backward compatibility), threaded through SuggestionCoordinator+Acceptance and the withCaretRect copy helper.
  • MirrorOverlayLayout.computeAnchorTopY groups .caretMidLine with .userPreference/.perAppOverride for caret-rect anchoring; seven new policy tests cover the full auto/always-inline/always-mirror/per-app matrix at both caret positions.

Confidence Score: 4/5

Safe to merge; the change is well-scoped, fully tested, and the backward-compatible default on isCaretAtEndOfLine preserves all existing call sites.

The logic is clean and the 7 new tests cover the full preference × caret-position matrix. The one doc comment in CompletionRenderMode.swift describes the wrong anchor rect for .caretMidLine (says field rect, implementation uses caret rect), which could mislead a future author working on FIM layout. No runtime behavior is affected.

Cotabby/Models/CompletionRenderMode.swift — the caretMidLine case doc needs the anchor-rect description corrected before FIM work builds on it.

Important Files Changed

Filename Overview
Cotabby/Models/CompletionRenderMode.swift Adds caretMidLine to MirrorReason; new case's doc comment incorrectly states anchoring target (field rect vs caret rect).
Cotabby/Support/CompletionRenderModePolicy.swift Refactors mode(for:bundleIdentifier:) into a public wrapper plus private preferenceMode; adds mid-line promotion that correctly overrides .inline only, preserving existing mirror reasons.
Cotabby/Support/MirrorOverlayLayout.swift Extends caret-rect anchoring to .caretMidLine alongside .userPreference/.perAppOverride; updated docs accurately describe the change.
Cotabby/Models/SuggestionModels.swift Adds isCaretAtEndOfLine to SuggestionOverlayGeometry with default true for backward compatibility; threaded correctly through init and withCaretRect.
Cotabby/App/Coordinators/SuggestionCoordinator+Acceptance.swift Threads isCaretAtEndOfLine from FocusedInputContext into SuggestionOverlayGeometry at the single presentOverlay definition site; all call sites inherit the value correctly.
CotabbyTests/CompletionRenderModePolicyTests.swift Seven new tests covering all four preference modes at mid-line caret, plus the explicit inline end-of-line pin; good matrix coverage.
CotabbyTests/CotabbyTestFixtures.swift Adds isCaretAtEndOfLine parameter to overlayGeometry fixture factory with true default; no issues.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[presentOverlay called] --> B[Build SuggestionOverlayGeometry\nwith isCaretAtEndOfLine from context]
    B --> C[CompletionRenderModePolicy.mode]
    C --> D[preferenceMode: check userPreference\n& per-app overrides]
    D --> E{effectivePreference}
    E -->|alwaysInline| F[.inline]
    E -->|alwaysMirror| G[.mirror reason: userPreference\nor perAppOverride]
    E -->|auto| H{caretQuality == .estimated?}
    H -->|yes| I[.mirror reason: caretGeometryEstimated]
    H -->|no| F
    F --> J{baseMode == .inline\nAND !isCaretAtEndOfLine?}
    G --> K[Return baseMode unchanged]
    I --> K
    J -->|yes| L[.mirror reason: caretMidLine]
    J -->|no| M[.inline]
    L --> N[MirrorOverlayLayout: anchor to caretRect\nfallback to inputFrameRect]
    M --> O[Inline ghost text at caret]
    K --> P{reason?}
    P -->|caretGeometryEstimated| Q[MirrorOverlayLayout: anchor to inputFrameRect\nfallback to caretRect offset]
    P -->|userPreference / perAppOverride / caretMidLine| N
Loading

Fix All in Codex Fix All in Claude Code

Reviews (1): Last reviewed commit: "feat(overlay): show popup card when care..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

When the caret sits mid-line (real characters follow it before the next line break), inline ghost text has no home: it would paint over the trailing characters. Promote any such inline presentation to the mirror card, which anchors to the caret line. This also establishes the surface fill-in-middle completions will render in.

The promotion overrides an explicit Inline preference too, since inline cannot render mid-line; presentations already routed to the card keep their original, more specific reason. Drives off the existing CaretLinePosition.isAtEndOfLine signal so an end-of-line caret with later paragraphs stays inline.

Adds isCaretAtEndOfLine to SuggestionOverlayGeometry (defaulted true), the .caretMidLine mirror reason, MirrorOverlayLayout anchoring for it, and 7 policy tests covering the auto/inline/mirror/per-app matrix.
Comment on lines +30 to +34
/// The caret sits mid-line: real characters follow it before the next line break. Inline
/// ghost text would draw on top of those trailing characters, so the suggestion is promoted
/// to the card, which anchors to the field rect instead of the caret. This is also the
/// surface fill-in-middle completions render in, since a FIM result has no inline home.
case caretMidLine
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The doc comment says the card "anchors to the field rect instead of the caret" for .caretMidLine, but MirrorOverlayLayout.computeAnchorTopY groups .caretMidLine with .userPreference and .perAppOverride, all of which anchor to the caret rect (falling back to the field rect only when the caret rect is empty). Since caret geometry is trustworthy in this case, anchoring to the caret is the intentional choice — the comment just describes the wrong rect.

Suggested change
/// The caret sits mid-line: real characters follow it before the next line break. Inline
/// ghost text would draw on top of those trailing characters, so the suggestion is promoted
/// to the card, which anchors to the field rect instead of the caret. This is also the
/// surface fill-in-middle completions render in, since a FIM result has no inline home.
case caretMidLine
/// The caret sits mid-line: real characters follow it before the next line break. Inline
/// ghost text would draw on top of those trailing characters, so the suggestion is promoted
/// to the card, which anchors to the caret rect (the geometry is trustworthy here), sitting
/// just under the cursor the same way an inline ghost would. This is also the surface
/// fill-in-middle completions render in, since a FIM result has no inline home.
case caretMidLine

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant